package sketch.specs;

import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.CharacterLiteral;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.internal.core.dom.NaiveASTFlattener;

import sketch.ast.ASTUtils;
import sketch.util.Checker;

public class SymbolicValueVisitor extends NaiveASTFlattener {
	
	private List<String> replaceMethodSignature = new LinkedList<String>();
	private final Map<String, Object> replacingMethodDefaultValues = new LinkedHashMap<String, Object>();
	private int numOfReplace = 0;
	
	//keep track o fthe parent
	private Set<MethodInvocation> symbolicMethodSet = new LinkedHashSet<MethodInvocation>();
	//keep track of the parent
	private Map<MethodInvocation, MethodDeclaration> symbolicMethodAndMethods = new LinkedHashMap<MethodInvocation, MethodDeclaration>();
	
	private Set<MethodInvocation> symbolicIntDeclarations = new LinkedHashSet<MethodInvocation>();
	
	/**
	 * Replaces the Values.symbolicInt() with 0
	 * */
	public SymbolicValueVisitor() {
		//for symbolic int
		this.replaceMethodSignature.add("Values.symbolicInt()");
		this.replaceMethodSignature.add("sketch.ounit.Values.symbolicInt()");
		
		//other cases, which are not frequently used, not well tested
		//add method signature to
		this.replacingMethodDefaultValues.put("Values.symbolicBoolean()", Boolean.valueOf(false));
		this.replacingMethodDefaultValues.put("sketch.ounit.Values.symbolicBoolean()", Boolean.valueOf(false));
//		this.replacingMethodDefaultValues.put("Values.symbolicInt()", 0);
//		this.replacingMethodDefaultValues.put("sketch.ounit.Values.symbolicInt()", 0);
		this.replacingMethodDefaultValues.put("Values.symbolicLong()", Long.valueOf(0l));
		this.replacingMethodDefaultValues.put("sketch.ounit.Values.symbolicLong()", Long.valueOf(0l));
		this.replacingMethodDefaultValues.put("Values.symbolicFloat()", Float.valueOf(0.0f));
		this.replacingMethodDefaultValues.put("sketch.ounit.Values.symbolicFloat()", Float.valueOf(0.0f));
		this.replacingMethodDefaultValues.put("Values.symbolicDouble()", Double.valueOf(0d));
		this.replacingMethodDefaultValues.put("sketch.ounit.Values.symbolicDouble()", Double.valueOf(0d));
		this.replacingMethodDefaultValues.put("Values.symbolicShort()", Short.valueOf("0"));
		this.replacingMethodDefaultValues.put("sketch.ounit.Values.symbolicShort()", Short.valueOf("0"));
		this.replacingMethodDefaultValues.put("Values.symbolicChar()", Character.valueOf('0'));
		this.replacingMethodDefaultValues.put("sketch.ounit.Values.symbolicChar()", Character.valueOf('0'));
		this.replacingMethodDefaultValues.put("Values.symbolicByte()", Byte.valueOf((byte)0));
		this.replacingMethodDefaultValues.put("sketch.ounit.Values.symbolicByte()", Byte.valueOf((byte)0));
	}
	
	public int getNumOfReplace() {
		return this.numOfReplace;
	}
	
	public Set<MethodInvocation> getSymbolicIntDeclarations() {
		return this.symbolicIntDeclarations;
	}
	
	public List<MethodInvocation> getSymbolicIntDeclarationsAsList() {
		Set<MethodInvocation> set = this.getSymbolicIntDeclarations();
		List<MethodInvocation> l = new LinkedList<MethodInvocation>(set);
		return l;
	}
	
	public String[] getSymbolicMethodSignatures() {
		String[] signatures = new String[symbolicMethodSet.size()];
		
		int count = 0;
		for(MethodInvocation methodInvoke: this.symbolicMethodSet) {
			signatures[count++] = symbolicMethodSignature(methodInvoke);
		}
		
		return signatures;
	}
	
	public String[] getSymbolicMethodSignaturesForMethod(String methodName) {
		Set<String> methodSet = new LinkedHashSet<String>();
		
		for(MethodInvocation methodInvoke : this.symbolicMethodSet) {
			MethodDeclaration methodDeclaration = this.symbolicMethodAndMethods.get(methodInvoke);
			Checker.checkNull(methodDeclaration, "The method declaration could not be null for: " + methodInvoke);
			if(methodDeclaration.getName().getIdentifier().equals(methodName)) {
				methodSet.add(this.symbolicMethodSignature(methodInvoke));
			}
		}
		
		return (String[])methodSet.toArray(new String[0]);
	}
	
	@Override
	public boolean visit(MethodInvocation method) {
		
		String signature = method.toString();
		
		//i separate the implementation of int and other values
		//this needs to be unified later
		if(this.replaceMethodSignature.contains(signature)) {
			//keep that
			this.symbolicIntDeclarations.add(method);
			//System.out.println(signature);
			Expression replacedInt = method.getAST().newNumberLiteral("0");
			Checker.checkTrue(method.getParent() instanceof MethodInvocation, "The symoblicInt() method is used in a wrong place.");
			MethodInvocation methodInvoke = (MethodInvocation)method.getParent();
			int indexOfArgument = methodInvoke.arguments().indexOf(method);
			Checker.checkTrue(indexOfArgument != -1, "The symoblicInt() should appear as one of its arguments.");
			//to see is this method invoke has been already added
			if(!isAlreadyAdded(methodInvoke)) {
				MethodInvocation cloneMethod = ASTUtils.deepClone(methodInvoke);
				this.symbolicMethodSet.add(cloneMethod);
				//compute the method
				MethodDeclaration parentMethod = ASTUtils.closeMethodDeclaration(methodInvoke);
				Checker.checkNull(parentMethod, "The method declaration should not be null: " + methodInvoke);
				this.symbolicMethodAndMethods.put(cloneMethod, parentMethod);
			}
			List<Expression> expressions = methodInvoke.arguments();
			//replace the argument
			expressions.remove(indexOfArgument);
			expressions.add(indexOfArgument, replacedInt);
			numOfReplace++;
		} else {
			//replace with other values
			if(this.replacingMethodDefaultValues.containsKey(signature)) {
				Checker.checkTrue(method.getParent() instanceof MethodInvocation, "The " + signature + " method is used in a wrong place: " +
						"it must be used inside a method invocation.");
				MethodInvocation methodInvoke = (MethodInvocation)method.getParent();
				int indexOfArgument = methodInvoke.arguments().indexOf(method);
				Checker.checkTrue(indexOfArgument != -1, "The method: " + signature + " should appear in the argument place.");
				//to see is this method invoke has been already added
				if(!isAlreadyAdded(methodInvoke)) {
					MethodInvocation cloneMethod = ASTUtils.deepClone(methodInvoke);
					this.symbolicMethodSet.add(cloneMethod);
					MethodDeclaration parentMethod = ASTUtils.closeMethodDeclaration(methodInvoke);
					Checker.checkNull(parentMethod, "The method declaration should not be null: " + methodInvoke);
					this.symbolicMethodAndMethods.put(cloneMethod, parentMethod);
				}
				List<Expression> expressions = methodInvoke.arguments();
				Expression replaced_arg = this.createArgumentExpression(method.getAST(), this.replacingMethodDefaultValues.get(signature));
				expressions.add(indexOfArgument, replaced_arg);
				numOfReplace++;
			}
		}
		
		return super.visit(method);
	}
	
	private Expression createArgumentExpression(AST ast, Object o) {
		Checker.checkNull(o, "The given object can not be null.");
		if(o instanceof Float|| o instanceof Long|| o instanceof Double
			|| o instanceof Short || o instanceof Byte) {
			return ast.newNumberLiteral(o.toString());
		} else if (o instanceof Boolean) {
			return ast.newBooleanLiteral((Boolean)o);
		} else if (o instanceof Character) {
			CharacterLiteral charLiteral = ast.newCharacterLiteral();
			charLiteral.setCharValue((Character)o);
			return charLiteral;
		} else {
			throw new RuntimeException("Unexpected type of o: " + o.getClass());
		}
	}
	
	private boolean isAlreadyAdded(MethodInvocation method) {
		boolean added = false;
		for(MethodInvocation invoke : this.symbolicMethodSet) {
			if(invoke.getStartPosition() == method.getStartPosition()
					&& method.getName().getIdentifier().equals(invoke.getName().getIdentifier())) {
				added = true;
				break;
			}
		}
		return added;
	}
	
	private String symbolicMethodSignature(MethodInvocation methodInvoke) {
		StringBuilder sb = new StringBuilder();
		sb.append(methodInvoke.getName().getIdentifier());
		Checker.checkTrue(methodInvoke.arguments().size() > 0, "There should be at least 1 argument for a symbolic method.");
		List<Expression> arguments = methodInvoke.arguments();
		sb.append("(");
		for(int i = 0; i < arguments.size(); i++) {
			if(i > 0) {
				sb.append("#");
			}
			Expression arg = arguments.get(i);
			if(this.replaceMethodSignature.contains(arg.toString())) {
				sb.append("sym");
			} else {
				sb.append("con");
			}
		}
		sb.append(")");
		
		//check the validity
		String retSignature = sb.toString();
		Checker.checkTrue(retSignature.indexOf("sym") != -1, "There is no symbolic args in method: " + retSignature);
		
		return retSignature;
	}
	
}